<?php


namespace paper\view\driver;


use JetBrains\PhpStorm\Pure;
use paper\exception\ParseException;

class Parse
{
    public $tagStack = [];


    private $options = [
        'prefix_path'  => '/',
        'source_path'  => '/view/',
        'tag_block'    => ['{%', '%}'],
        'tag_var'      => ['{$', '}'],
        'tag_function' => ['{:', '}'],
        'tag_comment'  => ['{*', "*}"]
    ];

    private array $regexes;
    private int $cursor = 0;
    private int $previousCursor = 0;
    private $source;
    public $compile = '';
    private $excerpt = '';
    private Interpreter $interpreter;
    private string|bool $extend = false;
    private mixed $sourcePath = '';

    #[Pure]
    public function __construct($options = [], $source = '')
    {
        $this->options     = array_merge($this->options, $options);
        $this->sourcePath  = $source;
        $this->interpreter = new Interpreter();
        $this->regexes     = [
            'comment_end'  => '#' . preg_quote($this->options['tag_comment'][1], '#') . '\s*#i',
            'var_end'      => '#' . preg_quote($this->options['tag_var'][1], '#') . '\s*#i',
            'block_end'    => '#' . preg_quote($this->options['tag_block'][1], '#') . '\s*#i',
            'function_end' => '#' . preg_quote($this->options['tag_function'][1], '#') . '\s*#i',
            'token_start'  => '#(?:' .
                preg_quote($this->options['tag_block'][0], '#') .
                '|' .
                preg_quote($this->options['tag_var'][0], '#') .
                '|' .
                preg_quote($this->options['tag_function'][0], '#') .
                '|' .
                preg_quote($this->options['tag_comment'][0], '#') .
                ')#is'
        ];
    }

    private function match($regexes, &$matches)
    {
        return preg_match($regexes, $this->source, $matches, PREG_OFFSET_CAPTURE, $this->cursor);
    }

    private function realPath($path)
    {
        return $this->options['prefix_path'] . $path;
    }

    private function moveCursor($offset)
    {
        $this->previousCursor = $this->cursor;
        $this->cursor         = $offset;
    }

    private function resetCursor()
    {
        $this->cursor  = 0; //重置游标
        $this->excerpt = ''; //清空text片段
    }

    private function findClosedToken($token)
    {
        if ($this->match($this->regexes[$token], $matches)) {
            $tokenLength = strlen($matches[0][0]);
            $token       = substr($this->source, $this->cursor, $matches[0][1] - $this->cursor);
            $this->moveCursor($matches[0][1] + $tokenLength);
            return trim($token);
        }
        return false;
    }


    function parseComment()
    {
        if (!$tag = $this->findClosedToken('comment_end')) {
            $this->parseException("Can't found the end token.");
        }
        return ['name' => 'comment', 'closed_tag' => false, 'expression' => $tag];
    }


    function parseFunction()
    {
        if (!$tag = $this->findClosedToken('function_end')) {
            $this->parseException("Can't found the end token.");
        }
        return ['name' => 'function', 'closed_tag' => false, 'expression' => $tag];
    }

    function parseBlock(): array
    {
        if (!$tag = $this->findClosedToken('block_end')) {
            $this->parseException("Can't found the end token.");
        }
        $expression = null;
        if (str_starts_with($tag, 'end')) {
            $name  = substr($tag, 3);
            $close = true;
            if (!isset($this->interpreter->tags[$name]))
                $this->parseException("Symbol error: undefined symbol {% $tag %}!");
        } else {
            $tmp  = explode(' ', $tag, 2);
            $name = $tmp[0];

            if (!isset($this->interpreter->tags[$name]))
                $this->parseException("Symbol error: undefined symbol {% $name %}!");

            $expression = $this->parseTagAttrs($name, trim($tmp[1] ?? ''));
            $close      = false;
        }
        return ['name' => $name, 'closed_tag' => $close, 'expression' => $expression];

    }

    function parseVar()
    {
        if (!$tag = $this->findClosedToken('var_end')) {
            throw new ParseException("Can't found the end token.");
        }
        return ['name' => 'variable', 'closed_tag' => false, 'expression' => $tag];
    }

    protected function parseTagAttrs($tagName, $expression)
    {
        $tag = $this->interpreter->tags[$tagName];
        if (!isset($tag['expression']) || !$tag['expression']) {
            $p     = '#(?P<name>[^=]+)\s*=(?>\s*)(["|\'])(?P<value>[^\2]+?)\2#i';
            $attrs = [];
            if (preg_match_all($p, $expression, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $attrs[$match['name']] = $match['value'];
                }
            }
            return $attrs;
        }
        return $expression;
    }

    private function pushTag($tag)
    {
        $this->tagStack[] = $tag;
    }

    private function popTag()
    {
        return array_pop($this->tagStack);
    }

    private function parseException($msg)
    {
        $file           = $this->extend ?: $this->sourcePath;
        $line           = substr_count($this->source, "\n", 0, $this->previousCursor) + 1;
        $parseException = new ParseException($msg);
        $parseException->setFile($file);
        $parseException->setLine($line);
        throw $parseException;
    }

    /**
     * 跳过标签不解析，主要用在
     * @param $name
     */
    private function findTag($name)
    {
        $regexes = '#' .
            preg_quote($this->options['tag_block'][0] . $name . $this->options['tag_block'][1], '#') .
            '#';
        if (!$this->match($regexes, $matches)) {
            $this->parseException("Can't found the end token!");
        }
        $this->excerpt = substr($this->source, $this->cursor, $matches[0][1] - $this->cursor);
        $this->moveCursor($matches[0][1] + strlen($matches[0][0]));
    }

    private function fetchOneTag()
    {
        if (!$this->match($this->regexes['token_start'], $matches)) {
            $this->excerpt = substr($this->source, $this->cursor);
            return null;
        }
        $this->excerpt = substr($this->source, $this->cursor, $matches[0][1] - $this->cursor);
        $this->moveCursor($matches[0][1] + strlen($matches[0][0]));

        /**
         * 判断获取的token开头是属于那种类型的命令
         * {% 块类型 {$ 变量类型 {: 函数类型 {* 注释类型
         */
        switch (trim($matches[0][0])) {
            case $this->options['tag_block'][0]:
                $tag = $this->parseBlock();
                break;
            case $this->options['tag_comment'][0]:
                $tag = $this->parseComment();
                break;
            case $this->options['tag_var'][0]:
                $tag = $this->parseVar();
                break;
            case $this->options['tag_function'][0]:
                $tag = $this->parseFunction();
                break;
        }
        $tag['offset'] = $matches[0][1];
        return $tag;
    }


    function parsing()
    {
        $out = '';
        //获取一个标签
        while ($tag = $this->fetchOneTag()) {
            //判断标签是否是闭合标签
            if (isset($tag['closed_tag']) && $tag['closed_tag']) {
                $stack = $this->popTag();
                if ($tag['name'] !== $stack['name']) {
                    $this->parseException("Symbol error: the expected symbol is {% end{$stack['name']} %}");
                }
                $out .= $this->excerpt;
                return $out;
            }

            /*
             * 不是闭合标签则执行此部分
             */

            $out .= $this->excerpt;

            // 遇到literal标签则直接跳过不解析
            // 目前还不支持嵌套
            if ($tag['name'] === 'raw') {
                $this->findTag('endraw');
                $out .= $this->excerpt;
                continue;
            }

            $attribute = $this->interpreter->tags[$tag['name']];
            $excerpt   = '';

            //如果这是一个需要闭合的标签 则将标签压入堆栈并且进行递归解析
            if (isset($attribute['need_closed']) && $attribute['need_closed']) {
                $this->pushTag($tag);
                $excerpt = $this->parsing();
            }
            $handle = 'parse' . ucfirst($tag['name']) . 'Tag';
            $out    .= call_user_func([$this->interpreter, $handle], $tag['expression'], $excerpt);
            continue;
        }

        if ($stack = $this->popTag()) {
            $this->previousCursor = $stack['offset'];
            $this->parseException("Symbol error: {% {$stack['name']} %} symbol not closed!");
        }
        $out .= $this->excerpt;
        return $out;
    }


    /**
     *
     * 解析继承的部分
     *
     */

    /**
     * @return string
     */
    public function getExtend(): string
    {
        return $this->extend;
    }

    /**
     * @param $file
     * @return bool|string
     * @throws ParseException
     */
    private function loadFile($file): bool|string
    {
        if (!is_file($file)) {
            $this->parseException("Unable to load file:" . $file);
        }
        return file_get_contents($file);
    }


    /**
     * 开始编译并且返回编译后的字符串
     * @return string
     */
    function compile(): string
    {
        $this->source = $this->loadFile($this->sourcePath);
        $res          = $this->parsing();
        //判断是否使用了继承
        if ($this->interpreter->useExtend()) {
            //重置解析的游标指针
            $this->resetCursor();
            $this->interpreter->setInChildren(false);
            $this->extend = $this->options['prefix_path'] . $this->options['source_path'] . $this->interpreter->getParent() . '.html';
            $this->source = $this->loadFile($this->extend);
            $res          = $this->parsing();
        }
        return $res;
    }
}